/**
 * Copyright (C) 2009-2013 FoundationDB, LLC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.foundationdb.rest.dml;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;

import com.foundationdb.ais.model.Column;
import com.foundationdb.ais.model.Table;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.foundationdb.ais.model.TableName;
import com.foundationdb.qp.operator.Operator;
import com.foundationdb.server.explain.ExplainContext;
import com.foundationdb.server.explain.format.DefaultFormatter;
import com.foundationdb.server.service.session.Session;
import com.foundationdb.server.test.it.ITBase;
import com.foundationdb.server.types.common.types.TypesTranslator;
import com.foundationdb.server.types.mcompat.mtypes.MTypesTranslator;
import com.foundationdb.server.types.service.TypesRegistryService;

public class InsertGeneratorIT extends ITBase {

    public static final String SCHEMA = "test";
    private InsertGenerator insertGenerator;
    
    protected TypesTranslator typesTranslator() {
        return MTypesTranslator.INSTANCE;
    }

    @After
    public void commit() {
        this.txnService().commitTransaction(this.session());
    }

    @Before
    public void start() {
        Session session = this.session();
        this.txnService().beginTransaction(session);
    }
    
    @Test
    public void testCInsert() {
        
        createTable(SCHEMA, "c",
                "cid INT PRIMARY KEY NOT NULL",
                "name VARCHAR(32)");

        TableName table = new TableName (SCHEMA, "c");
        this.insertGenerator = new InsertGenerator (this.ais());
        insertGenerator.setTypesRegistry(this.serviceManager().getServiceByClass(TypesRegistryService.class));
        insertGenerator.setTypesTranslator(this.typesTranslator());
        Operator insert = insertGenerator.create(table);
        
        assertEquals(
            "Project_Default(Field(0))\n" +
            "  Insert_Returning(INTO c)\n" +
            "    Project_Default(NULL, NULL)\n" +
            "      ValuesScan_Default([])",
            getExplain(insert, table.getSchemaName())
        );
    }

    @Test
    public void testNoPKInsert() {
        
        createTable (SCHEMA, "c", 
                "cid INT NOT NULL",
                "name VARCHAR(32)");
        TableName table = new TableName (SCHEMA, "c");
        this.insertGenerator = new InsertGenerator (this.ais());
        insertGenerator.setTypesRegistry(this.serviceManager().getServiceByClass(TypesRegistryService.class));
        insertGenerator.setTypesTranslator(this.typesTranslator());
        Operator insert = insertGenerator.create(table);
        
        assertEquals(
                "Insert_Returning(INTO c)\n" +
                "  Project_Default(NULL, NULL, _SEQ_NEXT(test, c___row_id_seq))\n" +
                "    ValuesScan_Default([])",
                getExplain(insert, table.getSchemaName())
        );
    }

    @Test
    public void testIdentityDefault() {
        createTable (SCHEMA, "c",
                "cid int NOT NULL PRIMARY KEY generated by default as identity",
                "name varchar(32) NOT NULL");
        
        TableName table = new TableName (SCHEMA, "c");
        this.insertGenerator = new InsertGenerator (this.ais());
        insertGenerator.setTypesRegistry(this.serviceManager().getServiceByClass(TypesRegistryService.class));
        insertGenerator.setTypesTranslator(this.typesTranslator());
        Operator insert = insertGenerator.create(table);

        assertEquals(
            "Project_Default(Field(0))\n" +
            "  Insert_Returning(INTO c)\n" +
            "    Project_Default(_SEQ_NEXT(test, c_cid_seq), NULL)\n" +
            "      ValuesScan_Default([])",
            getExplain(insert, table.getSchemaName())
        );
    }
    
    @Test
    public void testIdentityAlways() {
        createTable (SCHEMA, "c",
                "cid int NOT NULL PRIMARY KEY generated always as identity",
                "name varchar(32) NOT NULL");
        
        TableName table = new TableName (SCHEMA, "c");
        this.insertGenerator = new InsertGenerator (this.ais());
        insertGenerator.setTypesRegistry(this.serviceManager().getServiceByClass(TypesRegistryService.class));
        insertGenerator.setTypesTranslator(this.typesTranslator());
        Operator insert = insertGenerator.create(valueMap("c", "cid", "10"), table);
        assertEquals(
            "Project_Default(Field(0))\n" +
            "  Insert_Returning(INTO c)\n" +
            "    Project_Default(_SEQ_NEXT(test, c_cid_seq), NULL)\n" +
            "      ValuesScan_Default(['10'])",
            getExplain(insert, table.getSchemaName())
        );
    }
    
    @Test
    public void testDefaults() {
        createTable (SCHEMA, "c", 
                "cid int not null primary key default 0",
                "name varchar(32) not null default ''",
                "taxes double not null default '0.0'",
                "cdate date default current_date"
        );
        TableName table = new TableName (SCHEMA, "c");
        this.insertGenerator = new InsertGenerator (this.ais());
        insertGenerator.setTypesRegistry(this.serviceManager().getServiceByClass(TypesRegistryService.class));
        insertGenerator.setTypesTranslator(this.typesTranslator());
        Operator insert = insertGenerator.create(table);
        assertEquals(
            "Project_Default(Field(0))\n" +
            "  Insert_Returning(INTO c)\n" +
            "    Project_Default(0, '', 0.000000e+00, CURRENT_DATE())\n" +
            "      ValuesScan_Default([])",
            getExplain(insert, table.getSchemaName())
        );
        insert = insertGenerator.create(valueMap("c", "name", "foo", "cdate", "2014-02-03"), table);
        assertEquals(
            "Project_Default(Field(0))\n" +
            "  Insert_Returning(INTO c)\n" +
            "    Project_Default(0, Field(0), 0.000000e+00, Field(1))\n" +
            "      ValuesScan_Default(['foo', '2014-02-03'])",
            getExplain(insert, table.getSchemaName())
        );
    }
    
    @Test 
    public void testPKNotFirst() {
        createTable (SCHEMA, "c",
                "name varchar(32) not null",
                "address varchar(64) not null",
                "cid int not null primary key");
        TableName table = new TableName (SCHEMA, "c");
        this.insertGenerator = new InsertGenerator (this.ais());
        insertGenerator.setTypesRegistry(this.serviceManager().getServiceByClass(TypesRegistryService.class));
        insertGenerator.setTypesTranslator(this.typesTranslator());
        Operator insert = insertGenerator.create(table);
        assertEquals(
            "Project_Default(Field(2))\n" +
            "  Insert_Returning(INTO c)\n" +
            "    Project_Default(NULL, NULL, NULL)\n" +
            "      ValuesScan_Default([])",
            getExplain(insert, table.getSchemaName())
        );
    }
    
    @Test
    public void testPKMultiColumn() {
        createTable(SCHEMA, "o",
                "cid int not null",
                "oid int not null",
                "items int not null",
                "primary key (cid, oid)");
        TableName table = new TableName (SCHEMA, "o");
        this.insertGenerator = new InsertGenerator (this.ais());
        insertGenerator.setTypesRegistry(this.serviceManager().getServiceByClass(TypesRegistryService.class));
        insertGenerator.setTypesTranslator(this.typesTranslator());
        Operator insert = insertGenerator.create(table);
        assertEquals(
            "Project_Default(Field(0), Field(1))\n" +
            "  Insert_Returning(INTO o)\n" +
            "    Project_Default(NULL, NULL, NULL)\n" +
            "      ValuesScan_Default([])",
            getExplain(insert, table.getSchemaName())
        );
    }
    
    @Test
    public void testJoinedTable() {
        createTable(SCHEMA, "c",
                "cid int not null",
                "fist_name varchar(32)",
                "PRIMARY KEY(cid)");
        createTable (SCHEMA, "a",
                "aid int not null",
                "cid int not null",
                "state char(2)",
                "PRIMARY KEY (aid)",
                "GROUPING FOREIGN KEY (cid) REFERENCES c(cid)");
        TableName table = new TableName (SCHEMA, "a");
        this.insertGenerator = new InsertGenerator (this.ais());
        insertGenerator.setTypesRegistry(this.serviceManager().getServiceByClass(TypesRegistryService.class));
        insertGenerator.setTypesTranslator(this.typesTranslator());
        Operator insert = insertGenerator.create(table);
        assertEquals(
            "Project_Default(Field(0))\n" +
            "  Insert_Returning(INTO a)\n" +
            "    Project_Default(NULL, NULL, NULL)\n" +
            "      ValuesScan_Default([])",
            getExplain(insert, table.getSchemaName())
        );
    }

    @Test
    public void testOrdersTable() {
        createTable(SCHEMA, "customers",
                "cid int not null",
                "first_name varchar(32)",
                "primary key (cid)");
        createTable (SCHEMA, "orders",
                "oid int not null",
                "cid int not null",
                "odate datetime",
                "primary key (oid)",
                "grouping foreign key (cid) references customers(cid)");
        TableName table = new TableName (SCHEMA, "orders");
        this.insertGenerator = new InsertGenerator (this.ais());
        insertGenerator.setTypesRegistry(this.serviceManager().getServiceByClass(TypesRegistryService.class));
        insertGenerator.setTypesTranslator(this.typesTranslator());
        Operator insert = insertGenerator.create(table);
        assertEquals(
            "Project_Default(Field(0))\n" +
            "  Insert_Returning(INTO orders)\n" +
            "    Project_Default(NULL, NULL, NULL)\n" +
            "      ValuesScan_Default([])",
            getExplain(insert, table.getSchemaName())
        );
    }
    
    @Test
    public void testAllTypes() {
        createTable (SCHEMA, "all_types",
                "year_field year",
                "bigint_field bigint",
                "bigint_unsigned_field bigint unsigned",
                "blob_field blob",
                "boolean_field boolean",
                "char_field char",
                "char_multi_field char(32)",
                "clob_field clob",
                "date_field date",
                "decimal_field decimal(10,0)",
                "double_field double",
                "float_field float",
                "integer_field integer",
                "numeric_field numeric(10,0)",
                "real_field real",
                "smallint_field smallint",
                "time_field time",
                "timestamp_field timestamp",
                "varchar_field varchar(32)",
                "datetime_field datetime");
        TableName table = new TableName (SCHEMA, "all_types");
        this.insertGenerator = new InsertGenerator (this.ais());
        insertGenerator.setTypesRegistry(this.serviceManager().getServiceByClass(TypesRegistryService.class));
        insertGenerator.setTypesTranslator(this.typesTranslator());
        Operator insert = insertGenerator.create(table);
        assertEquals(
            "Insert_Returning(INTO all_types)\n" +
            "  Project_Default(NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, _SEQ_NEXT(test, all_types___row_id_seq))\n" +
            "    ValuesScan_Default([])",
            getExplain(insert, table.getSchemaName())
        );
    }

    protected String getExplain (Operator plannable, String defaultSchemaName) {
        StringBuilder str = new StringBuilder();
        ExplainContext context = new ExplainContext(); // Empty
        DefaultFormatter f = new DefaultFormatter(defaultSchemaName);
        for (String operator : f.format(plannable.getExplainer(context))) {
            if(str.length() > 0) {
                str.append("\n");
            }
            str.append(operator);
        }
        return str.toString();
    }

    private Map<Column, String> valueMap(String tableName, String... colsAndValues) {
        assert (colsAndValues.length % 2) == 0;
        Table table = ais().getTable(SCHEMA, tableName);
        Map<Column, String> outMap = new LinkedHashMap<>();
        for(int i = 0; i < colsAndValues.length; i += 2) {
            String col = colsAndValues[i];
            String value = colsAndValues[i + 1];
            outMap.put(table.getColumn(col), value);
        }
        return outMap;
    }
}
